package com.virtual.repository.common.queries;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import com.virtual.repository.common.Constants;
import com.virtual.repository.common.criteria.Criterion;
import com.virtual.repository.utils.ReflectionUtil;

/**
 * A builder for the <code>Query</code> objects ensuring full syntactical correctness.
 * @author Zielony
 * @version 1.0
 */
public class QueryBuilder implements IQueryBuilder {
	//TODO: Please add also ability to add complex property names such as: customer.address.street
	/**
	 * The <code>class</code> for the instances of which the query will be looking for.
	 */
	private Class<?> queryClass;
	
	/**
	 * A <code>List<code> of the names of the properties to be set in the results.
	 */
	private List<String> properties;
	
	/**
	 * A <code>Map</code> of the selection criteria.
	 * Indexed by criterion's property name.
	 */
	private List<Criterion> criteria;
	
	/**
	 * The maximal number of returned results.
	 */
	private Integer limit = null;
	
	//Specific implementation used to enable cloning -> defensive copy.
	/**
	 * 
	 */
	private HashMap<String, Boolean> orderByMap;
	
	/**
	 * Constructor.
	 * @param queryClass the <code>class</code> for the instances of which the query will be looking for.
	 */
	public QueryBuilder(Class<?> queryClass) {
		/*
		 * null is not accepted as the argument since we cannot query for no table at all. 
		 */
		if(queryClass == null) {
			throw new NullPointerException("Null class passed to the QueryBuilder as the queryClass");
		}
		
		this.queryClass = queryClass;
	}
	
	/* (non-Javadoc)
	 * @see com.virtual.repository.common.queries.IQueryBuilder#addProperty(java.lang.String)
	 */
	@Override
	public void addProperty(String property) {
		/*
		 * Checking if the queried class contains a property with the given name.
		 */
		String propPrepared = property.replaceAll("\\.", Constants.NESTED_FIELDS_SEPARATOR);
		try{
			ReflectionUtil.getDeclaredField(this.queryClass, propPrepared);
		} catch (NoSuchFieldException ex) {
			throw new IllegalArgumentException("The given property: " + property + " is not a member of the queried class");
		}
		
		/* 
		 * Lazily intializing the properties list.
		 */
		this.getProperties().add(propPrepared);
	}
	
	/* (non-Javadoc)
	 * @see com.virtual.repository.common.queries.IQueryBuilder#setLimit(java.lang.Integer)
	 */
	@Override
	public void setLimit(Integer limit) {
		/*
		 * Limit lesser or equal to zero would not be logically correct while null means there is no limit. 
		 */
		if(limit != null && limit <= 0 ) {
			throw new IllegalArgumentException("The limit for query results' number negative or zero");
		}
		
		this.limit = limit;
	}
	
	/* (non-Javadoc)
	 * @see com.virtual.repository.common.queries.IQueryBuilder#addOrderByRule(java.lang.String, boolean)
	 */
	@Override
	public void addOrderByRule(String property, boolean reverse) {
		/* 
		 * Impossible to add a sort rule on the property which is not queried.
		 */
		if(properties == null || !properties.contains(property)) {
			throw new IllegalStateException("The given property: " + property + " is not declared to be selected and thus cannot be an ordering argument.");
		}
		 
		this.getOrderByMap().put(property, reverse);
	}
	
	/* (non-Javadoc)
	 * @see com.virtual.repository.common.queries.IQueryBuilder#addCriteria(java.util.Collection)
	 */
	@Override
	public void addCriteria(Collection<Criterion> criteria) {
		/*
		 * Variables used for checking criteria correctness in the current context. 
		 */
		Iterator<Criterion> criteriaIterator = criteria.iterator();
		String iProperty;
		
		/*
		 * Pre-checking the criteria. 
		 */
		while(criteriaIterator.hasNext()) {
			
			iProperty = criteriaIterator.next().getProperty();
			
			/*
			 * If the class does not contain any of the properties on which the criteria are set, that is an 
			 * exception.
			 */
			try {
				ReflectionUtil.getDeclaredField(this.queryClass, iProperty);
			} catch (NoSuchFieldException ex) {
				throw new IllegalArgumentException("One of the criteria refers to the property not present in the queries class");
			}
		}
		
		this.getCriteria().addAll(criteria);
	}

	/* (non-Javadoc)
	 * @see com.virtual.repository.common.queries.IQueryBuilder#build()
	 */
	@Override
	public Query build() {
		/*
		 * Assembling a new Query while making defensive copy of all sets.
		 */
		
		return new Query(queryClass, this.getProperties(), this.getCriteria(), limit, this.getOrderByMap());
	}

	public List<String> getProperties() {
		//using lazy initialization
		if (this.properties == null) {
			this.properties = new ArrayList<>();
		}
		
		return this.properties;
	}

	public HashMap<String, Boolean> getOrderByMap() {
		//using lazy initialization
		if(this.orderByMap == null) {
			this.orderByMap = new HashMap<>();
		}
		
		return this.orderByMap;
	}

	public List<Criterion> getCriteria() {
		//using lazy initialization
		if(this.criteria == null) {
			this.criteria = new ArrayList<>();
		}
		
		return this.criteria;
	}
}